package com.miniapp.service.impl;

import cn.hutool.core.util.StrUtil;
import com.miniapp.service.FileStorageService;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * MinIO存储服务实现类
 *
 * <p>基于MinIO对象存储的具体实现，负责文件的上传、下载和删除操作
 * <p>当存储目录为空时，自动创建以当前日期为格式的路径
 *
 * @author yourName
 * @date 2025-09-06
 */
@Service
public class MinioStorageServiceImpl implements FileStorageService {

    /**
     * 日期格式化器，用于生成目录名，格式为yyyy/MM/dd
     */
    private static final Logger log = LoggerFactory.getLogger(MinioStorageServiceImpl.class);

    /**
     * 时间偏差补偿（毫秒）
     */
    @Value("${minio.time-offset-ms:0}")
    private long timeOffsetMs;

    private final MinioClient minioClient;
    private final String bucketName;
    private final String endpoint;
    private final String accessKey;
    private final String secretKey;
    private final int defaultExpireMinutes;

    public MinioStorageServiceImpl(
            @Value("${minio.endpoint}") String endpoint,
            @Value("${minio.access-key}") String accessKey,
            @Value("${minio.secret-key}") String secretKey,
            @Value("${minio.bucket-name}") String bucketName,
            @Value("${minio.temporary-url.expire-minutes:30}") int defaultExpireMinutes) {

        // 验证并清理参数（使用Hutool工具类）
        this.accessKey = StrUtil.trimToEmpty(accessKey);
        this.secretKey = StrUtil.trimToEmpty(secretKey);
        this.bucketName = StrUtil.trimToEmpty(bucketName);
        this.defaultExpireMinutes = defaultExpireMinutes;
        this.endpoint = cleanEndpoint(endpoint);

        // 初始化MinIO客户端
        this.minioClient = MinioClient.builder()
                .endpoint(this.endpoint)
                .credentials(accessKey.trim(), secretKey.trim())
                .build();
    }

    /**
     * 上传文件（自动处理文件名和类型）
     *
     * @param file 上传的文件，不能为空
     * @param dir  存储目录，为空时自动使用当前日期路径（yyyy/MM/dd）
     * @return 存储结果映射，包含fileKey、fileUrl等信息
     */
    @Override
    public Map<String, String> upload(MultipartFile file, String dir) {
        // 处理目录：为空则使用当前日期路径
        String storageDir = resolveStorageDir(dir);
        // 生成带UUID的文件名，避免重复
        String originalFileName = file.getOriginalFilename();
        String customFileName = UUID.randomUUID() + "_" + originalFileName;
        return upload(file, storageDir, customFileName, null);
    }

    /**
     * 上传文件（指定文件名）
     *
     * @param file           上传的文件，不能为空
     * @param dir            存储目录，为空时自动使用当前日期路径（yyyy/MM/dd）
     * @param customFileName 自定义存储文件名，不能为空
     * @return 存储结果映射，包含fileKey、fileUrl等信息
     */
    @Override
    public Map<String, String> upload(MultipartFile file, String dir, String customFileName) {
        // 处理目录：为空则使用当前日期路径
        String storageDir = resolveStorageDir(dir);
        return upload(file, storageDir, customFileName, null);
    }

    /**
     * 上传文件（全参数控制）
     *
     * @param file           上传的文件，不能为空
     * @param dir            存储目录，为空时自动使用当前日期路径（yyyy/MM/dd）
     * @param customFileName 自定义存储文件名，可为空（将自动生成）
     * @param contentType    文件类型，可为空（将自动提取）
     * @return 存储结果映射，包含fileKey、fileUrl等信息
     */
    @Override
    public Map<String, String> upload(MultipartFile file, String dir, String customFileName, String contentType) {
        // 参数校验：文件不能为空
        if (file.isEmpty()) {
            throw new IllegalArgumentException("上传文件不能为空");
        }

        // 处理目录：为空则使用当前日期路径
        String storageDir = resolveStorageDir(dir);

        try (InputStream inputStream = file.getInputStream()) {
            // 处理文件名
            String fileName = buildFileName(file, customFileName);
            // 处理文件类型
            String fileContentType = buildContentType(file, contentType);
            // 构建存储路径
            String fileKey = buildFileKey(storageDir, fileName);

            // 执行上传
            this.minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(fileKey)
                            .stream(inputStream, file.getSize(), -1)
                            .contentType(fileContentType)
                            .build()
            );

            log.info("文件上传成功，fileKey: {}", fileKey);

            // 构建返回结果
            return buildResultMap(file, fileKey, fileName, fileContentType);
        } catch (Exception e) {
            log.error("MinIO文件上传失败", e);
            throw new RuntimeException("MinIO文件上传失败：" + e.getMessage(), e);
        }
    }

    /**
     * 通过输入流上传文件
     *
     * @param inputStream 文件输入流，不能为空
     * @param fileName    文件名，不能为空
     * @param contentType 文件类型，可为空
     * @param dir         存储目录，为空时自动使用当前日期路径
     * @return 存储结果映射
     */
    @Override
    public Map<String, String> upload(InputStream inputStream, String fileName, String contentType, String dir) {
        // 参数校验
        if (inputStream == null) {
            throw new IllegalArgumentException("文件输入流不能为空");
        }
        if (fileName == null || fileName.trim().isEmpty()) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        // 处理目录
        String storageDir = resolveStorageDir(dir);
        // 处理文件类型
        String fileContentType = contentType != null ? contentType : "application/octet-stream";
        // 构建存储路径
        String fileKey = buildFileKey(storageDir, fileName);

        try {
            // 执行上传（注意：MinIO需要知道文件大小，这里假设使用available()获取，实际可能需要优化）
            this.minioClient.putObject(
                    PutObjectArgs.builder()
                            .bucket(bucketName)
                            .object(fileKey)
                            .stream(inputStream, inputStream.available(), -1)
                            .contentType(fileContentType)
                            .build()
            );

            log.info("通过输入流上传文件成功，fileKey: {}", fileKey);

            // 构建返回结果
            Map<String, String> result = new HashMap<>(4);
            result.put("fileKey", fileKey);
            result.put("fileUrl", getFileUrl(fileKey));
            result.put("fileName", fileName);
            result.put("contentType", fileContentType);

            return result;
        } catch (Exception e) {
            log.error("通过输入流上传文件失败", e);
            throw new RuntimeException("通过输入流上传文件失败：" + e.getMessage(), e);
        }
    }

    /**
     * 下载文件
     *
     * @param fileKey 存储服务中的文件标识，不能为空
     * @return 文件输入流，需调用方自行关闭
     */
    @Override
    public InputStream download(String fileKey) {
        if (fileKey == null || fileKey.trim().isEmpty()) {
            throw new IllegalArgumentException("文件标识不能为空");
        }

        try {
            log.info("下载文件，fileKey: {}", fileKey);
            return this.minioClient.getObject(
                    GetObjectArgs.builder()
                            .bucket(bucketName)
                            .object(fileKey)
                            .build()
            );
        } catch (Exception e) {
            log.error("MinIO文件下载失败，fileKey: {}", fileKey, e);
            throw new RuntimeException("MinIO文件下载失败：" + e.getMessage(), e);
        }
    }

    /**
     * 删除文件
     *
     * @param fileKey 存储服务中的文件标识，不能为空
     * @return 是否删除成功
     */
    @Override
    public boolean delete(String fileKey) {
        if (fileKey == null || fileKey.trim().isEmpty()) {
            throw new IllegalArgumentException("文件标识不能为空");
        }

        try {
            this.minioClient.removeObject(
                    RemoveObjectArgs.builder()
                            .bucket(bucketName)
                            .object(fileKey)
                            .build()
            );
            log.info("文件删除成功，fileKey: {}", fileKey);
            return true;
        } catch (Exception e) {
            log.error("MinIO文件删除失败，fileKey: {}", fileKey, e);
            throw new RuntimeException("MinIO文件删除失败：" + e.getMessage(), e);
        }
    }

    /**
     * 获取文件访问URL
     *
     * @param fileKey 存储服务中的文件标识，不能为空
     * @return 文件可访问的URL
     */
    @Override
    public String getFileUrl(String fileKey) {
        if (fileKey == null || fileKey.trim().isEmpty()) {
            throw new IllegalArgumentException("文件标识不能为空");
        }

        // 构建MinIO文件访问URL
        // 格式：endpoint/bucketName/fileKey
        StringBuilder urlBuilder = new StringBuilder(endpoint);
        if (!endpoint.endsWith("/")) {
            urlBuilder.append("/");
        }
        urlBuilder.append(bucketName).append("/").append(fileKey);

        String fileUrl = urlBuilder.toString();
        log.debug("生成文件访问URL，fileKey: {}, url: {}", fileKey, fileUrl);
        return fileUrl;
    }

    /**
     * 解析存储目录
     *
     * <p>若目录为空或空白字符串，则自动生成当前日期路径（格式：yyyy/MM/dd）
     *
     * @param dir 原始目录
     * @return 处理后的存储目录
     */
    private String resolveStorageDir(String dir) {
        // 目录为空或空白字符串时，使用当前日期作为目录
        if (dir == null || dir.trim().isEmpty()) {
            return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        }
        return dir;
    }

    /**
     * 构建存储文件名
     *
     * <p>若自定义文件名为空，则生成UUID+原始文件名的组合，避免重名
     *
     * @param file           上传的文件
     * @param customFileName 自定义文件名，可为空
     * @return 处理后的文件名
     */
    private String buildFileName(MultipartFile file, String customFileName) {
        if (customFileName != null && !customFileName.trim().isEmpty()) {
            return customFileName;
        }
        // 自动生成文件名：UUID + 原始文件名
        return UUID.randomUUID() + "_" + file.getOriginalFilename();
    }

    /**
     * 构建文件类型
     *
     * <p>优先级：手动指定的类型 > 自动提取的类型 > 默认二进制流类型
     *
     * @param file        上传的文件
     * @param contentType 手动指定的文件类型，可为空
     * @return 最终使用的文件类型
     */
    private String buildContentType(MultipartFile file, String contentType) {
        if (contentType != null && !contentType.trim().isEmpty()) {
            return contentType;
        }
        // 自动提取文件类型，默认二进制流
        String fileContentType = file.getContentType();
        return fileContentType != null ? fileContentType : "application/octet-stream";
    }

    /**
     * 构建存储路径
     *
     * <p>将目录和文件名拼接为完整路径，自动处理目录结尾的斜杠
     *
     * @param dir      存储目录
     * @param fileName 文件名
     * @return 完整存储路径
     */
    private String buildFileKey(String dir, String fileName) {
        // 处理目录结尾的斜杠
        return dir.endsWith("/") ? dir + fileName : dir + "/" + fileName;
    }

    /**
     * 构建返回结果映射
     *
     * @param file        上传的文件
     * @param fileKey     存储标识
     * @param fileName    存储文件名
     * @param contentType 文件类型
     * @return 包含文件信息的结果映射
     */
    private Map<String, String> buildResultMap(MultipartFile file, String fileKey, String fileName, String contentType) {
        Map<String, String> result = new HashMap<>(6);
        result.put("fileKey", fileKey);
        // 使用新增的getFileUrl方法
        result.put("fileUrl", getFileUrl(fileKey));
        result.put("fileName", fileName);
        result.put("originalFileName", file.getOriginalFilename());
        result.put("contentType", contentType);
        result.put("fileSize", String.valueOf(file.getSize()));
        return result;
    }

    /**
     * 生成临时访问URL（严格遵循签名规范）
     */
    @Override
    public String generateTemporaryUrl(String fileKey, String contentType, String fileName) {
        try {
            // 使用 GetPresignedObjectUrlArgs.Builder 构建参数
            GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                    .method(Method.GET)
                    .bucket(bucketName)
                    .object(fileKey)
                    .expiry(30, TimeUnit.MINUTES)
                    .extraQueryParams(createExtraQueryParams(contentType, fileName))
                    .build();

            return minioClient.getPresignedObjectUrl(args);
        } catch (Exception e) {
            // 更精细的异常处理建议：记录日志并抛出自定义异常
            throw new RuntimeException("生成预签名URL时发生错误: " + e.getMessage(), e);
        }
    }

    @Override
    public boolean copy(String sourceKey, String targetKey) {
        try {
            // 使用MinIO的服务器端复制功能
            minioClient.copyObject(
                    CopyObjectArgs.builder()
                            .bucket(bucketName)
                            .object(targetKey)
                            .source(
                                    CopySource.builder()
                                            .bucket(bucketName)
                                            .object(sourceKey)
                                            .build()
                            )
                            .build()
            );
            return true;
        } catch (MinioException | InvalidKeyException | NoSuchAlgorithmException |
                IOException | IllegalArgumentException e) {
            log.error("MinIO文件复制失败: 源={}/{} -> 目标={}/{}", bucketName, sourceKey, bucketName, targetKey, e);
            return false;
        }
    }

    private Map<String, String> createExtraQueryParams(String contentType, String fileName) throws Exception {
        Map<String, String> extraQueryParams = new HashMap<>();
        if (contentType != null && !contentType.trim().isEmpty()) {
            // 指定服务器返回的Content-Type
            extraQueryParams.put("response-content-type", contentType);
        }
        if (fileName != null && !fileName.trim().isEmpty()) {
            // 指定浏览器将文件下载到本地，并设置默认文件名
            // 需要对文件名进行URL编码以防止特殊字符问题
            String encodedFileName = java.net.URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
            extraQueryParams.put("response-content-disposition", "attachment; filename=\"" + encodedFileName + "\"");
        }
        return extraQueryParams;
    }

    /**
     * 清理端点URL，去除尾部斜杠
     */
    private String cleanEndpoint(String endpoint) {
        if (endpoint == null) {
            return "";
        }
        // 去除URL尾部的斜杠，避免签名计算差异
        return endpoint.replaceAll("/+$", "");
    }

}
